Coverage Report

Created: 2026-03-18 12:57

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
D:\a\scloud-dns\scloud-dns\src\utils\logging.rs
Line
Count
Source
1
use crate::config::{LogFormat, LogLevel, LoggingConfig};
2
use crate::exceptions::SCloudException;
3
use crate::utils::time::{format_system_time, now_epoch_ms};
4
5
use once_cell::sync::OnceCell;
6
use serde_json::json;
7
8
use std::fs::{self, OpenOptions};
9
use std::io::{self, Write};
10
use std::path::{Path, PathBuf};
11
use std::sync::{Mutex, OnceLock};
12
use std::time::SystemTime;
13
14
use tokio::sync::mpsc;
15
16
struct Logger {
17
    cfg: LoggingConfig,
18
    file: std::fs::File,
19
}
20
21
pub struct OtelLog {
22
    pub target: String,
23
    pub severity: &'static str,
24
    pub message: String,
25
    pub timestamp_unix_nano: String,
26
}
27
28
pub static LOG_SENDER: OnceCell<mpsc::Sender<OtelLog>> = OnceCell::new();
29
static LOGGER: OnceLock<Mutex<Logger>> = OnceLock::new();
30
31
4
pub fn build_otlp_payload(logs: &[OtelLog]) -> serde_json::Value {
32
4
    json!({
33
4
        "resourceLogs": [{
34
4
            "resource": {
35
4
                "attributes": [
36
4
                    { "key": "service.name",        "value": { "stringValue": "scloud-dns" } },
37
4
                    { "key": "service.instance.id", "value": { "stringValue": "scloud-dns-01" } }
38
                ]
39
            },
40
4
            "scopeLogs": [{
41
4
                "scope": { "name": "scloud.logger", "version": "0.2.2" },
42
9
                
"logRecords"4
:
logs4
.
iter4
().
map4
(|log| {
43
9
                    json!({
44
9
                        "timeUnixNano": log.timestamp_unix_nano,
45
9
                        "severityText": log.severity,
46
9
                        "body": { "stringValue": log.message },
47
9
                        "attributes": [
48
9
                            { "key": "target", "value": { "stringValue": log.target } }
49
                        ]
50
                    })
51
9
                }).
collect4
::<Vec<_>>()
52
            }]
53
        }]
54
    })
55
4
}
56
/// Initialize global logger.
57
/// Call once at startup.
58
1
pub fn init(cfg: LoggingConfig) -> Result<(), SCloudException> {
59
1
    let path = Path::new(&cfg.file);
60
61
1
    if let Some(parent) = path.parent() {
62
1
        fs::create_dir_all(parent).map_err(|e| 
{0
63
0
            eprintln!("failed to create log dir {:?}: {}", parent, e);
64
0
            SCloudException::SCLOUD_LOGGING_PATH_CREATION_FAILED
65
0
        })?;
66
0
    }
67
68
1
    let file = OpenOptions::new()
69
1
        .create(true)
70
1
        .append(true)
71
1
        .open(path)
72
1
        .map_err(|_| SCloudException::SCLOUD_LOGGING_FILE_CREATION_OR_OPENING_FAILED)
?0
;
73
74
1
    let logger = Logger { cfg, file };
75
1
    let _ = LOGGER.set(Mutex::new(logger));
76
1
    Ok(())
77
1
}
78
79
/// Write one log line (internal).
80
/// Safe to call from any thread.
81
25
pub fn log(level: LogLevel, target: &str, msg: &str) {
82
25
    let Some(lock) = LOGGER.get() else {
83
0
        return;
84
    };
85
86
25
    let mut g = lock.lock().unwrap_or_else(|poisoned| 
poisoned0
.
into_inner0
());
87
88
25
    if level < g.cfg.level {
89
0
        return;
90
25
    }
91
92
25
    if g.cfg.live_print {
93
0
        let now = SystemTime::now();
94
0
        println!(
95
0
            "[{}][{:?}][{}] - {}",
96
0
            format_system_time(now),
97
0
            level,
98
0
            target,
99
0
            msg
100
0
        );
101
25
    }
102
103
25
    if g.cfg.rotate {
104
0
        let max_bytes = g.cfg.max_size_mb.saturating_mul(1024 * 1024);
105
0
        if max_bytes > 0 {
106
0
            if let Ok(meta) = g.file.metadata() {
107
0
                if meta.len() >= max_bytes {
108
0
                    let _ = rotate_file(&mut *g);
109
0
                }
110
0
            }
111
0
        }
112
25
    }
113
114
25
    let line = match g.cfg.format {
115
0
        LogFormat::JSON => format_json_line(level, target, msg),
116
25
        LogFormat::TEXT => format_text_line(level, target, msg),
117
    };
118
119
25
    let _ = g.file.write_all(line.as_bytes());
120
25
    let _ = g.file.write_all(b"\n");
121
25
    let _ = g.file.flush();
122
25
}
123
124
0
fn rotate_file(logger: &mut Logger) -> io::Result<()> {
125
0
    let path = Path::new(&logger.cfg.file);
126
127
0
    let epoch_ms = now_epoch_ms();
128
0
    let rotated = rotated_name(path, epoch_ms);
129
130
0
    let _ = logger.file.flush();
131
0
    let _ = fs::rename(path, &rotated);
132
133
0
    logger.file = OpenOptions::new().create(true).append(true).open(path)?;
134
0
    Ok(())
135
0
}
136
137
0
fn rotated_name(path: &Path, epoch_ms: u128) -> PathBuf {
138
0
    let mut p = path.as_os_str().to_owned();
139
0
    let suffix = format!(".{}", epoch_ms);
140
0
    p.push(suffix);
141
0
    PathBuf::from(p)
142
0
}
143
144
25
fn format_text_line(level: LogLevel, target: &str, msg: &str) -> String {
145
25
    format!(
146
        "[{}][{}][{}] {}",
147
25
        format_system_time(SystemTime::now()),
148
25
        level.as_str(),
149
        target,
150
        msg
151
    )
152
25
}
153
154
0
fn format_json_line(level: LogLevel, target: &str, msg: &str) -> String {
155
0
    let msg_esc = json_escape(msg);
156
0
    let target_esc = json_escape(target);
157
0
    format!(
158
        r#"{{"ts":{},"level":"{}","target":"{}","msg":"{}"}}"#,
159
0
        now_epoch_ms(),
160
0
        level.as_str(),
161
        target_esc,
162
        msg_esc
163
    )
164
0
}
165
166
0
fn json_escape(s: &str) -> String {
167
0
    let mut out = String::with_capacity(s.len() + 8);
168
0
    for c in s.chars() {
169
0
        match c {
170
0
            '"' => out.push_str("\\\""),
171
0
            '\\' => out.push_str("\\\\"),
172
0
            '\n' => out.push_str("\\n"),
173
0
            '\r' => out.push_str("\\r"),
174
0
            '\t' => out.push_str("\\t"),
175
0
            c if c.is_control() => {
176
                use std::fmt::Write as _;
177
0
                let _ = write!(out, "\\u{:04x}", c as u32);
178
            }
179
0
            _ => out.push(c),
180
        }
181
    }
182
0
    out
183
0
}
184
185
#[macro_export]
186
macro_rules! __log_internal {
187
    ($lvl:expr, $otel_lvl:expr, $($arg:tt)*) => {{
188
        let target = concat!(module_path!(), ":", line!());
189
190
        if $otel_lvl != "TRACE" || $otel_lvl != "DEBUG" {
191
            if $otel_lvl == "STRACE" {
192
                if let Some(sender) = $crate::utils::logging::LOG_SENDER.get() {
193
                    let _ = sender.try_send($crate::utils::logging::OtelLog {
194
                        target: target.to_string(),
195
                        severity: "TRACE",
196
                        message: format!($($arg)*),
197
                        timestamp_unix_nano: $crate::utils::time::now_unix_nano(),
198
                    });
199
                }
200
            } else if $otel_lvl == "SDEBUG" {
201
                if let Some(sender) = $crate::utils::logging::LOG_SENDER.get() {
202
                    let _ = sender.try_send($crate::utils::logging::OtelLog {
203
                        target: target.to_string(),
204
                        severity: "DEBUG",
205
                        message: format!($($arg)*),
206
                        timestamp_unix_nano: $crate::utils::time::now_unix_nano(),
207
                    });
208
                }
209
            }
210
            else {
211
                if let Some(sender) = $crate::utils::logging::LOG_SENDER.get() {
212
                    let _ = sender.try_send($crate::utils::logging::OtelLog {
213
                        target: target.to_string(),
214
                        severity: $otel_lvl,
215
                        message: format!($($arg)*),
216
                        timestamp_unix_nano: $crate::utils::time::now_unix_nano(),
217
                    });
218
                }
219
            }
220
        }
221
222
        $crate::utils::logging::log(
223
            $lvl,
224
            target,
225
            &format!($($arg)*),
226
        );
227
    }};
228
}
229
230
#[macro_export]
231
macro_rules! log_trace {
232
    ($($arg:tt)*) => {
233
        $crate::__log_internal!($crate::config::LogLevel::TRACE, "TRACE", $($arg)*);
234
    };
235
}
236
237
#[macro_export]
238
macro_rules! log_strace {
239
    ($($arg:tt)*) => {
240
        $crate::__log_internal!($crate::config::LogLevel::TRACE, "STRACE", $($arg)*);
241
    };
242
}
243
244
#[macro_export]
245
macro_rules! log_debug {
246
    ($($arg:tt)*) => {
247
        $crate::__log_internal!($crate::config::LogLevel::DEBUG, "DEBUG", $($arg)*);
248
    };
249
}
250
251
#[macro_export]
252
macro_rules! log_sdebug {
253
    ($($arg:tt)*) => {
254
        $crate::__log_internal!($crate::config::LogLevel::DEBUG, "SDEBUG", $($arg)*);
255
    };
256
}
257
258
#[macro_export]
259
macro_rules! log_info {
260
    ($($arg:tt)*) => {
261
        $crate::__log_internal!($crate::config::LogLevel::INFO, "INFO", $($arg)*);
262
    };
263
}
264
265
#[macro_export]
266
macro_rules! log_warn {
267
    ($($arg:tt)*) => {
268
        $crate::__log_internal!($crate::config::LogLevel::WARN, "WARN", $($arg)*);
269
    };
270
}
271
272
#[macro_export]
273
macro_rules! log_error {
274
    ($($arg:tt)*) => {
275
        $crate::__log_internal!($crate::config::LogLevel::ERROR, "ERROR", $($arg)*);
276
    };
277
}
278
279
#[macro_export]
280
macro_rules! log_fatal {
281
    ($($arg:tt)*) => {
282
        $crate::__log_internal!($crate::config::LogLevel::FATAL, "FATAL", $($arg)*);
283
    };
284
}